﻿// SKNotes - a note taking and gathering tool
// Copyright (C) 2009  Stefan Kueng
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace SKNotes
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Text;
    using ICSharpCode.SharpZipLib.Zip;
    using SKNotes.CustomControls;
    using SKNotes.CustomControls.TaskDialog;
    using SKNotes.Utilities;

    public class Note : BindableObject
    {
        private string doc;
        private Hashtable noteData = new Hashtable();
        private InvokeOC<NoteTag> tags = new InvokeOC<NoteTag>(App.Current.Dispatcher);
        private InvokeOC<string> attachements = new InvokeOC<string>(App.Current.Dispatcher);
        private InvokeOC<string> inlinefiles = new InvokeOC<string>(App.Current.Dispatcher);
        private string path;
        private NoteBook noteBook;
        private DeferredAction deferredAction;
        private string id;
        private string tagsstring;

        public Note()
        {
            this.CreationDate = DateTime.Now;
            this.ModificationDate = DateTime.Now;
            this.id = System.Guid.NewGuid().ToString();
            this.path = @"D:\Development\SKNotes\testfiles\" + NoteList.Instance.CurrentNotebook.NoteBookName + @"\" + this.id + @".skn";
            this.tags.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(this.Tags_CollectionChanged);
            this.NoteBook = NoteList.Instance.CurrentNotebook;
            this.NoteBook.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.NoteBook_PropertyChanged);
        }

        public Note(string path, NoteBook noteBook)
        {
            this.path = path;
            this.id = System.IO.Path.GetFileNameWithoutExtension(path);
            using (ZipInputStream s = new ZipInputStream(File.OpenRead(this.DataPath)))
            {
                ZipEntry theEntry;
                while ((theEntry = s.GetNextEntry()) != null)
                {
                    if (theEntry.Name == "notedata")
                    {
                        BinaryFormatter b = new BinaryFormatter();
                        this.noteData = (Hashtable)b.Deserialize(s);
                        try
                        {
                            this.tags = (InvokeOC<NoteTag>)b.Deserialize(s);
                        }
                        catch (System.Runtime.Serialization.SerializationException)
                        {
                        }
                        finally
                        {
                            NoteList.WalkNoteTags(this.tags, (obj) =>
                            {
                                ((NoteTag)obj).NoteBook = noteBook;
                                return false;
                            });
                        }
                        break;
                    }
                }
                s.Close();
            }

            using (ZipInputStream s = new ZipInputStream(File.OpenRead(this.path)))
            {
                ZipEntry theEntry;
                while ((theEntry = s.GetNextEntry()) != null)
                {
                    if ((theEntry.IsDirectory) && (theEntry.Name.StartsWith("attachments")))
                    {
                        this.attachements.Add(System.IO.Path.GetFileName(theEntry.Name));
                    }
                }
                s.Close();
            }

            NoteList.WalkTags(this.tags, (obj) => { ((NoteTag)obj).PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.Tag_PropertyChanged); return false; });

            this.tags.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(this.Tags_CollectionChanged);
            this.NoteBook = noteBook;
            this.NoteBook.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.NoteBook_PropertyChanged);
        }

        public Note(BinaryFormatter b, Stream s, InvokeOC<NoteBook> books)
        {
            this.noteData = (Hashtable)b.Deserialize(s);
            this.path = (string)b.Deserialize(s);
            this.id = System.IO.Path.GetFileNameWithoutExtension(path);
            string noteBookName = (string)b.Deserialize(s);
            int count = (int)b.Deserialize(s);
            NoteBook nb = books.FirstOrDefault((obj) => ((NoteBook)obj).NoteBookName == noteBookName);
            this.noteBook = nb;
            for (int i = 0; i < count; ++i)
            {
                string tagpath = (string)b.Deserialize(s);
                // first check whether the tag already exists
                NoteTag tag = NoteList.WalkNoteTags(nb.Tags, (obj) => ((NoteTag)obj).TagPath == tagpath);
                if (tag == null)
                    tag = new NoteTag(tagpath, this.NoteBook);
                if (tag != null)
                {
                    if (NoteList.AddTag(this.Tags, tag))
                    {
                        tag.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.Tag_PropertyChanged);
                        this.RaisePropertyChanged("Tags");
                    }
                }
            }

            this.attachements = (InvokeOC<string>)b.Deserialize(s);
            this.inlinefiles = (InvokeOC<string>)b.Deserialize(s);
            this.tags.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(this.Tags_CollectionChanged);
            this.NoteBook.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.NoteBook_PropertyChanged);
        }

        public string ID
        {
            get { return this.id; }
        }

        public string Document
        {
            get
            {
                if (new FileInfo(path).Exists)
                {
                    using (ZipInputStream s = new ZipInputStream(File.OpenRead(this.path)))
                    {
                        ZipEntry theEntry;
                        while ((theEntry = s.GetNextEntry()) != null)
                        {
                            if (theEntry.Name == "content")
                            {
                                StreamReader sr = new StreamReader(s);
                                this.doc = sr.ReadToEnd();
                                sr.Close();
                                break;
                            }
                        }
                        s.Close();
                    }
                }

                if (new FileInfo(this.DataPath).Exists)
                {
                    using (ZipInputStream s = new ZipInputStream(File.OpenRead(this.DataPath)))
                    {
                        ZipEntry theEntry;
                        while ((theEntry = s.GetNextEntry()) != null)
                        {
                            if (theEntry.Name == "notedata")
                            {
                                BinaryFormatter b = new BinaryFormatter();
                                this.noteData = (Hashtable)b.Deserialize(s);
                                try
                                {
                                    this.tags = (InvokeOC<NoteTag>)b.Deserialize(s);
                                }
                                catch (System.Runtime.Serialization.SerializationException)
                                {
                                }
                                finally
                                {
                                    foreach (NoteTag t in this.tags)
                                    {
                                        t.NoteBook = this.noteBook;
                                    }
                                }
                                break;
                            }
                        }
                        s.Close();
                    }
                }

                return this.doc;
            }

            set
            {
                this.doc = value;
                this.RaisePropertyChanged("Document");
            }
        }

        public string Path
        {
            get
            {
                return this.path;
            }

            set
            {
                this.path = value;
                this.RaisePropertyChanged("Path");
            }
        }

        public string DataPath
        {
            get { return this.path.Replace(@".skn", @".skd"); }
        }

        public string Name
        {
            get
            {
                return (string)this.noteData["noteTitle"];
            }

            set
            {
                if ((string)this.noteData["noteTitle"] != value)
                {
                    this.noteData["noteTitle"] = value;
                    this.RaisePropertyChanged("Name");
                    this.DeferSaveNoteData();
                }
            }
        }

        public string SourceURL
        {
            get
            {
                return (string)this.noteData["sourceURL"];
            }

            set
            {
                if ((string)this.noteData["sourceURL"] != value)
                {
                    this.noteData["sourceURL"] = value;
                    this.RaisePropertyChanged("SourceURL");
                    this.DeferSaveNoteData();
                }
            }
        }

        public DateTime CreationDate
        {
            get
            {
                return (DateTime)this.noteData["noteCreationDate"];
            }

            set
            {
                this.noteData["noteCreationDate"] = value;
                this.RaisePropertyChanged("CreationDate");
            }
        }

        public DateTime ModificationDate
        {
            get
            {
                return (DateTime)this.noteData["noteModificationDate"];
            }

            set
            {
                this.noteData["noteModificationDate"] = value;
                this.RaisePropertyChanged("ModificationDate");
            }
        }

        public NoteBook NoteBook
        {
            get
            {
                return this.noteBook;
            }

            set
            {
                this.noteBook = value;
                this.RaisePropertyChanged("NoteBook");
            }
        }

        public InvokeOC<NoteTag> Tags
        {
            get { return this.tags; }
        }

        public List<string> TagsList
        {
            get
            {
                List<string> tagslist = new List<string>();
                NoteList.WalkNoteTags(this.tags, (obj) => { tagslist.Add(((NoteTag)obj).TagPath); return false; });
                return tagslist;
            }
        }

        public string TagsString
        {
            get
            {
                if (this.tagsstring == null)
                {
                    StringBuilder builder = new StringBuilder();
                    NoteList.WalkNoteTags(this.tags, (obj) =>
                    {
                        if (builder.Length != 0)
                        {
                            builder.Append(", ");
                        }
                        builder.Append(((NoteTag)obj).TagPath);
                        return false;
                    });
                    this.tagsstring = builder.ToString();
                }
                return tagsstring;
            }
        }

        public InvokeOC<string> Attachments
        {
            get { return this.attachements; }
        }

        public InvokeOC<string> InlineFiles
        {
            get { return this.inlinefiles; }
        }

        public bool Save()
        {
            bool ret = true;
            try
            {
                ZipFile zipFile = null;
                if (File.Exists(this.path))
                {
                    zipFile = new ZipFile(this.path);
                }
                else
                {
                    zipFile = ZipFile.Create(this.path);
                }

                using (zipFile)
                {
                    zipFile.BeginUpdate();
                    TempFile tempFile = new TempFile();
                    using (FileStream fs = File.Create(tempFile.Path))
                    {
                        Byte[] info = new UTF8Encoding(true).GetBytes(this.doc);
                        fs.Write(info, 0, info.Length);
                    }
                    zipFile.Add(tempFile.Path, "content");
                    zipFile.CommitUpdate();
                    ret = true;

                    zipFile.Close();
                }

                using (ZipOutputStream s = new ZipOutputStream(File.Open(this.DataPath, FileMode.OpenOrCreate)))
                {
                    s.SetLevel(9); // 0 - store only to 9 - means best compression

                    ZipEntry entry = new ZipEntry("notedata");

                    this.ModificationDate = DateTime.Now;
                    entry.DateTime = DateTime.Now;
                    s.PutNextEntry(entry);

                    BinaryFormatter b = new BinaryFormatter();
                    b.Serialize(s, this.noteData);
                    b.Serialize(s, this.tags);
                    s.Close();

                    s.Finish();
                    s.Close();
                }
            }
            catch (System.Exception e)
            {
                Debug.Print(e.Message);
                ret = false;
            }

            return ret;
        }

        public bool SaveNoteData()
        {
            bool ret = true;
            try
            {
                using (ZipOutputStream s = new ZipOutputStream(File.Open(this.DataPath, FileMode.OpenOrCreate)))
                {
                    s.SetLevel(9); // 0 - store only to 9 - means best compression

                    ZipEntry entry = new ZipEntry("notedata");

                    this.ModificationDate = DateTime.Now;
                    entry.DateTime = DateTime.Now;
                    s.PutNextEntry(entry);

                    BinaryFormatter b = new BinaryFormatter();
                    b.Serialize(s, this.noteData);
                    b.Serialize(s, this.tags);
                    s.Close();

                    s.Finish();
                    s.Close();
                }
            }
            catch (System.Exception e)
            {
                TaskDialog.ShowException(e);
                ret = false;
            }

            return ret;
        }

        public void Serialize(BinaryFormatter b, Stream s)
        {
            b.Serialize(s, this.noteData);
            b.Serialize(s, this.path);
            b.Serialize(s, this.noteBook.NoteBookName);
            b.Serialize(s, this.tags.Count);
            foreach (NoteTag tag in this.tags)
            {
                b.Serialize(s, tag.TagPath);
            }

            b.Serialize(s, this.attachements);
            b.Serialize(s, this.inlinefiles);
        }

        public bool AddTag(NoteTag tag)
        {
            // first check whether the tag already exists
            if (NoteList.AddTag(this.Tags, tag))
            {
                this.NoteBook.AddTag(tag);
                tag.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.Tag_PropertyChanged);
                this.DeferSaveNoteData();
                this.RaisePropertyChanged("Tags");
                this.tagsstring = null;
                this.RaisePropertyChanged("TagsString");
                this.RaisePropertyChanged("TagsList");
                return true;
            }

            return false;
        }

        public bool RemoveTag(NoteTag tag)
        {
            if (NoteList.RemoveTag(this.Tags, tag))
            {
                tag.PropertyChanged -= new System.ComponentModel.PropertyChangedEventHandler(this.Tag_PropertyChanged);
                this.DeferSaveNoteData();
                this.RaisePropertyChanged("Tags");
                this.tagsstring = null;
                this.RaisePropertyChanged("TagsString");
                this.RaisePropertyChanged("TagsList");
                return true;
            }

            return false;
        }

        public bool AddAttachment(string filePath)
        {
            bool ret = false;

            string filename = System.IO.Path.GetFileName(filePath);
            string escapedName = Uri.EscapeUriString(filename);

            ZipFile zipFile = null;
            if (File.Exists(this.path))
            {
                zipFile = new ZipFile(this.path);
            }
            else
            {
                zipFile = ZipFile.Create(this.path);
            }

            using (zipFile)
            {
                zipFile.BeginUpdate();
                zipFile.Add(filePath, "attachments/" + escapedName);
                zipFile.CommitUpdate();
                ret = true;

                zipFile.Close();
                ret = true;
                this.attachements.Add(filename);
                this.RaisePropertyChanged("Attachments");
            }
            return ret;
        }

        public bool RemoveAttachment(string fileName)
        {
            bool ret = false;

            using (ZipFile s = new ZipFile(File.Open(this.path, FileMode.OpenOrCreate)))
            {
                string escapedName = Uri.EscapeUriString(fileName);
                ZipEntry entry = s.GetEntry("attachments/" + escapedName);

                if (entry != null)
                {
                    s.BeginUpdate();
                    s.Delete(entry);
                    s.CommitUpdate();
                    this.attachements.Remove(fileName);
                    this.RaisePropertyChanged("Attachments");
                    ret = true;
                }

                s.Close();
            }

            return ret;
        }

        public bool SaveAttachment(string attachmentName, string attachmentPath)
        {
            bool ret = false;

            string escapedName = Uri.EscapeUriString(attachmentName);
            using (ZipInputStream s = new ZipInputStream(File.OpenRead(this.path)))
            {
                ZipEntry theEntry;
                while ((theEntry = s.GetNextEntry()) != null)
                {
                    if (theEntry.Name == "attachments/" + escapedName)
                    {
                        using (FileStream fileStream = new FileStream(attachmentPath, FileMode.Create, FileAccess.ReadWrite))
                        {
                            const int BufSize = 0x1000;
                            byte[] buf = new byte[BufSize];
                            int bytesRead = 0;
                            while ((bytesRead = s.Read(buf, 0, BufSize)) > 0)
                            {
                                fileStream.Write(buf, 0, bytesRead);
                            }
                            ret = true;
                        }
                        break;
                    }
                }
                s.Close();
            }
            return ret;
        }

        public bool AddInlineFile(string filePath, string name)
        {
            bool ret = false;
            ZipFile zipFile = null;

            string filename = System.IO.Path.GetFileName(filePath);

            if (File.Exists(this.path))
            {
                zipFile = new ZipFile(this.path);
            }
            else
            {
                zipFile = ZipFile.Create(this.path);
            }

            using (zipFile)
            {
                string escapedName = Uri.EscapeUriString(name);

                zipFile.BeginUpdate();
                zipFile.Add(filePath, "inlines/" + escapedName);
                zipFile.CommitUpdate();
                this.inlinefiles.Add(name);
                ret = true;

                zipFile.Close();
            }

            return ret;
        }



        public bool RemoveInlineFile(string fileName)
        {
            bool ret = false;

            using (ZipFile s = new ZipFile(File.Open(this.path, FileMode.OpenOrCreate)))
            {
                string escapedName = Uri.EscapeUriString(fileName);
                ZipEntry entry = s.GetEntry("inlines/" + escapedName);

                if (entry != null)
                {
                    s.BeginUpdate();
                    s.Delete(entry);
                    s.CommitUpdate();
                    this.inlinefiles.Remove(fileName);
                    ret = true;
                }

                s.Close();
            }

            return ret;
        }

        private void DeferSaveNoteData()
        {
            if (this.deferredAction == null)
            {
                this.deferredAction = DeferredAction.Create(() => this.SaveNoteData());
            }

            this.deferredAction.Defer(new TimeSpan(0, 0, 5));
        }

        private void Tag_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            this.DeferSaveNoteData();
        }

        private void Tags_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            this.DeferSaveNoteData();
            this.tagsstring = null;
            this.RaisePropertyChanged("TagsString");
            this.RaisePropertyChanged("TagsList");
        }

        private void NoteBook_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            // adjust the path of the note file
            this.path = NoteList.GetNotebookDirPath() + "\\" + NoteBook.NoteBookName + "\\" + System.IO.Path.GetFileName(this.path);
        }
    }
}
